﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;

namespace IWGAI.Demo
{
    public class LLMParser : MonoBehaviour
    {
        public static LLMParser Instance;

        [System.Serializable]
        public class NPCInfo
        {
            public string name;
            public string description;
        }

        [System.Serializable]
        public class ModelReply
        {
            public string answer;
            public string location;
            public NPCInfo npc;
        }

        private Dictionary<string, PendingScene> pendingScenes = new(StringComparer.OrdinalIgnoreCase);
        private Queue<(SceneData originScene, NPCData originNpc)> pendingOrigins = new();

        public SceneData CurrentScene { get; private set; }
        public NPCData CurrentNpc { get; private set; }

        void Awake()
        {
            if (Instance == null) Instance = this;
            else Destroy(gameObject);
        }

        void Start()
        {
            if (LLMClient.Instance != null)
            {
                LLMClient.Instance.OnRateLimit += HandleRateLimit;
            }
        }
        private void HandleRateLimit(int waitSeconds, string msg)
        {
            Debug.LogWarning($"[LLMParser] Rate limit hit. Waiting {waitSeconds}s");
            if (ChatManager.Instance != null)
            {
                ChatManager.Instance.AddMessage(
                    $"<color=grey>(The world grows quiet… try again in {waitSeconds} seconds.)</color>");
            }
        }

        // ------------------------------------------------------------
        // MAIN ENTRY POINTS
        // ------------------------------------------------------------
        public void BeginConversation(NPCData npc, SceneData scene)
        {
            if (npc == null) return;

            CurrentNpc = npc;
            CurrentScene = scene;
            pendingOrigins.Enqueue((scene, npc));

            string personality = string.IsNullOrEmpty(npc.personality)
                ? "A helpful and lively character."
                : npc.personality;

            npc.systemPrompt =
                $"You are {npc.displayName}. Stay fully in character and reply ONLY in JSON format. " +
                "Always include an 'answer' field for your dialogue. " +
                "If the player asks about a specific person, creature, or goal, and that entity should appear, " +
                "create that NPC or scene directly — do NOT redirect them endlessly. " +
                "Include 'location' and 'npc' ONLY when sending the player somewhere new. " +
                "If the player asks for a specific character or entity (like a dragon, blacksmith, or king), " +
                "generate that entity immediately as the 'npc' in your reply and place them in an appropriate 'location'. " +
                "Never invent unnecessary intermediaries. " +
                "Never include your own name in the 'npc' field. " +
                "Respond only in JSON. " +
                "Below is the current world log of events you know about. Use it as context but speak as yourself:\n" +
                WorldLog.GetContext(25); // add global log context here

            //  Build prompt: personal memory + system prompt
            List<LLMClient.Message> messages = new();
            messages.Add(new LLMClient.Message("system", npc.systemPrompt));

            int start = Mathf.Max(0, npc.memory.Count - 15);
            for (int i = start; i < npc.memory.Count; i++)
                messages.Add(npc.memory[i]);

            var userMsg = new LLMClient.Message("user", $"Greet the player as {npc.displayName}. Reply now in character.");
            messages.Add(userMsg);
            npc.memory.Add(userMsg);

            LLMClient.Instance.SendMessages(messages, OnRawResponse);

            Debug.Log($"[LLMParser] Started conversation with {npc.displayName} ({scene?.description ?? "no scene"})");
        }


        public void SendUserMessage(string userText)
        {
            if (string.IsNullOrEmpty(userText) || CurrentNpc == null) return;

            pendingOrigins.Enqueue((CurrentScene, CurrentNpc));

            var userMsg = new LLMClient.Message("user", userText);
            CurrentNpc.memory.Add(userMsg);
            WorldLog.Add(userMsg);

            List<LLMClient.Message> convo = new();
            convo.Add(new LLMClient.Message("system",
                CurrentNpc.systemPrompt + "\n" +
                "Remember: you are still " + CurrentNpc.displayName +
                ". Stay in character and respond with JSON. " +
                "Use the following global world log for context of all events so far:\n" +
                WorldLog.GetContext(25)));

            int start = Mathf.Max(0, CurrentNpc.memory.Count - 15);
            for (int i = start; i < CurrentNpc.memory.Count; i++)
                convo.Add(CurrentNpc.memory[i]);

            LLMClient.Instance.SendMessages(convo, OnRawResponse);
        }

        // ------------------------------------------------------------
        // RESPONSE HANDLING
        // ------------------------------------------------------------
        private void OnRawResponse(string raw)
        {
            (SceneData originScene, NPCData originNpc) origin = (null, null);
            if (pendingOrigins.Count > 0)
                origin = pendingOrigins.Dequeue();
            else
            {
                origin.originScene = CurrentScene;
                origin.originNpc = CurrentNpc;
            }

            StartCoroutine(ProcessAssistantContentCoroutine(raw, origin.originScene, origin.originNpc));
        }

        private string ExtractEntityFromText(string text)
        {
            if (string.IsNullOrEmpty(text)) return null;

            // crude extraction — looks for “the X” or “a X” patterns
            var m = Regex.Match(text, @"\b(?:the|a|an)\s+([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)", RegexOptions.IgnoreCase);
            if (m.Success)
                return m.Groups[1].Value.Trim();

            // fallback: last noun-like capitalized word
            var words = text.Split(' ');
            for (int i = words.Length - 1; i >= 0; i--)
            {
                if (!string.IsNullOrEmpty(words[i]) && char.IsUpper(words[i][0]))
                    return words[i];
            }
            return null;
        }


        private IEnumerator ProcessAssistantContentCoroutine(string rawContent, SceneData originScene, NPCData originNpc)
        {
            if (string.IsNullOrEmpty(rawContent)) yield break;

            Debug.Log("[LLMParser] Raw assistant content:\n" + rawContent);

            string jsonBlock = SanitizeJsonString(rawContent);
            ModelReply reply = null;

            if (!string.IsNullOrEmpty(jsonBlock))
            {
                try
                {
                    reply = JsonUtility.FromJson<ModelReply>(jsonBlock);
                    // If JSON had "npc": "Name" (string) JsonUtility will leave npc == null.
                    if (reply != null && reply.npc == null)
                    {
                        // Try to extract a string-valued npc field and convert to NPCInfo
                        var m = Regex.Match(jsonBlock, "\"npc\"\\s*:\\s*\"([^\"]+)\"", RegexOptions.IgnoreCase);
                        if (m.Success)
                        {
                            reply.npc = new NPCInfo { name = m.Groups[1].Value.Trim(), description = "" };
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.LogWarning("[LLMParser] JsonUtility.FromJson failed: " + ex.Message);
                    reply = null;
                }
            }

            if (reply == null)
            {
                // treat everything as chat text (no generation)
                reply = new ModelReply { answer = rawContent.Trim(), location = null, npc = null };
            }

            // Normalize reply fields to avoid NREs later
            if (reply.npc == null) reply.npc = new NPCInfo { name = null, description = null };
            if (reply.answer == null) reply.answer = "";

            // Prevent self-referential scene creation if reply references the origin NPC or scene
            if (!string.IsNullOrEmpty(reply.location) && originNpc != null)
            {
                bool sameNpc = !string.IsNullOrEmpty(reply.npc?.name) &&
                        reply.npc.name.Equals(originNpc.displayName, StringComparison.OrdinalIgnoreCase);

                bool mentionsNpc = !string.IsNullOrEmpty(originNpc.displayName) &&
                                   reply.location.IndexOf(originNpc.displayName, StringComparison.OrdinalIgnoreCase) >= 0;

                if (sameNpc || mentionsNpc)
                {
                    Debug.Log("[LLMParser] Ignoring self-referential location: " + reply.location);
                    reply.location = null;
                    reply.npc = new NPCInfo { name = null, description = null };
                }
            }

            // Generic placeholder NPC detection -> try to infer concrete target from player's last message
            if (!string.IsNullOrEmpty(reply.npc?.name))
            {
                string lowerNpc = reply.npc.name.ToLowerInvariant();
                if (lowerNpc.Contains("messenger") || lowerNpc.Contains("hermit") ||
                    lowerNpc.Contains("traveler") || lowerNpc.Contains("someone") ||
                    lowerNpc.Contains("figure") || lowerNpc.Contains("man") || lowerNpc.Contains("woman"))
                {
                    Debug.Log("[LLMParser] Generic or placeholder NPC detected, attempting to replace with concrete role.");
                    if (originNpc != null && originNpc.memory != null)
                    {
                        var lastPlayerMsg = originNpc.memory.FindLast(m => m.role == "user");
                        if (lastPlayerMsg != null)
                        {
                            string focus = ExtractEntityFromText(lastPlayerMsg.content);
                            if (!string.IsNullOrEmpty(focus))
                                reply.npc.name = focus;
                        }
                    }
                }
            }

            // Persist assistant reply to memory & world log
            originNpc?.memory.Add(new LLMClient.Message("assistant", reply.answer));
            WorldLog.Add(new LLMClient.Message("assistant", reply.answer ?? ""));

            if (ChatManager.Instance != null)
            {
                string label = originNpc != null ? originNpc.displayName : "NPC";
                ChatManager.Instance.AddMessage($"<color=yellow>{label}:</color> {reply.answer}");
            }

            // --- Prevent generation for greetings / same-location replies ---
            if (originScene != null && !string.IsNullOrEmpty(reply.location))
            {
                string currentSceneName = originScene.description?.Trim().ToLower() ?? "";
                string newLocationName = reply.location?.Trim().ToLower() ?? "";

                bool sameLocation = !string.IsNullOrEmpty(newLocationName) && currentSceneName.Contains(newLocationName);

                // Consider short greeting answers as non-generation (common startup case)
                string ansLower = reply.answer.ToLower();
                bool isGreeting = (ansLower.Contains("welcome") || ansLower.Contains("hello") || ansLower.Contains("greet"))
                                  && reply.answer.Length < 200 && !ansLower.Contains("go") && !ansLower.Contains("follow");

                if (sameLocation || isGreeting)
                {
                    Debug.Log("[LLMParser] Ignoring initial greeting or same-location generation: " + reply.location);
                    reply.location = null;
                    reply.npc = new NPCInfo { name = null, description = null };
                }
            }

            // --- Early exit if no world generation needed ---
            bool hasLocation = !string.IsNullOrEmpty(reply.location);
            bool hasNpc = !string.IsNullOrEmpty(reply.npc?.name);
            if (!hasLocation && !hasNpc)
                yield break;

            // Attempt to infer npc from player message if missing
            if (!hasNpc && originNpc != null && originNpc.memory != null)
            {
                var lastUser = originNpc.memory.FindLast(m => m.role == "user");
                if (lastUser != null)
                {
                    string inferred = ExtractEntityFromText(lastUser.content);
                    if (!string.IsNullOrEmpty(inferred))
                    {
                        reply.npc.name = inferred;
                        hasNpc = true;
                        Debug.Log("[LLMParser] Inferred missing NPC from player text: " + inferred);
                    }
                }
            }

            // --- CASE 1: Scene-only generation ---
            if (hasLocation && !hasNpc)
            {
                // if model explicitly says "no npc" or similar, skip
                if (reply.location.ToLower().Contains("no npc") || reply.location.ToLower().Contains("none"))
                {
                    Debug.Log("[LLMParser] Model requested scene with no NPC. Skipping NPC creation.");
                    yield break;
                }

                string locName = reply.location.Trim();
                Debug.Log("[LLMParser] Scene-only generation for location: " + locName);

                SceneData generatedScene = ScriptableObject.CreateInstance<SceneData>();
                generatedScene.id = "scene_" + locName.Replace(" ", "_").ToLower();
                generatedScene.description = "Auto-generated scene: " + locName;
                SceneManager.Instance.RegisterPendingScene(locName, generatedScene, originScene);

                // Use a safe label for background-only announcements
                AnnouncementPair bgPair = AnnouncementsManager.Instance.CreatePair("Background", locName);

                StartCoroutine(ImageGenerator.Instance.GenerateSceneImage("Background of " + locName, tex =>
                {
                    generatedScene.background = tex;
                    AnnouncementsManager.Instance.MarkAsLoaded(bgPair.bgUI);
                    TryUnlockExit(generatedScene, locName, originScene);
                }));

                yield break;
            }

            // --- CASE 2: NPC-only generation ---
            if (!hasLocation && hasNpc)
            {
                string npcToSpawn = reply.npc.name.Trim();
                if (string.IsNullOrEmpty(npcToSpawn)) yield break;

                Debug.Log("[LLMParser] NPC-only generation for: " + npcToSpawn);
                SceneData currentScene = originScene ?? SceneManager.Instance.GetCurrentScene();

                AnnouncementPair npcPair = AnnouncementsManager.Instance.CreatePair(npcToSpawn, "(current scene)");

                NPCData newCharacter = ScriptableObject.CreateInstance<NPCData>();
                newCharacter.id = "npc_" + npcToSpawn.Replace(" ", "_").ToLower();
                newCharacter.displayName = npcToSpawn;
                newCharacter.personality = "Generated NPC";
                if (currentScene.npcs == null) currentScene.npcs = new List<NPCData>();
                currentScene.npcs.Add(newCharacter);

                StartCoroutine(ImageGenerator.Instance.GenerateNpcImage(
                    $"Character sprite of {npcToSpawn}. Semi-realistic anime style, visual novel NPC. Transparent background PNG. NPC description: {reply.npc.description ?? ""}.",
                    sprite =>
                    {
                        newCharacter.portrait = sprite;
                        AnnouncementsManager.Instance.MarkAsLoaded(npcPair.npcUI);
                        TryUnlockExit(currentScene, currentScene.description, originScene);
                    }));

                yield break;
            }

            // --- CASE 3: Both location and NPC ---
            string locationName = reply.location?.Trim();
            string npcName = reply.npc?.name?.Trim();

            if (string.IsNullOrEmpty(locationName) || string.IsNullOrEmpty(npcName))
            {
                Debug.LogWarning("[LLMParser] Both location and npc were expected but one is missing. Aborting generation.");
                yield break;
            }

            if (pendingScenes.ContainsKey(locationName))
            {
                Debug.Log($"[LLMParser] Scene '{locationName}' already generating, skipping duplicate request.");
                yield break;
            }

            SceneData existingScene = SceneManager.Instance.GetScene(locationName);
            if (existingScene != null)
            {
                Debug.Log($"[LLMParser] Reusing existing scene: {locationName}");
                TryUnlockExit(existingScene, locationName, originScene);
                yield break;
            }

            SceneData fullScene = ScriptableObject.CreateInstance<SceneData>();
            fullScene.id = "scene_" + locationName.Replace(" ", "_").ToLower();
            fullScene.description = "Auto-generated scene: " + locationName;
            fullScene.npcs = new List<NPCData>();
            fullScene.exits = new List<string>();

            PendingScene pending = new PendingScene(locationName, fullScene, originScene);
            pendingScenes[locationName] = pending;

            AnnouncementPair fullPair = AnnouncementsManager.Instance?.CreatePair(npcName, locationName);

            NPCData generatedNpc = ScriptableObject.CreateInstance<NPCData>();
            generatedNpc.id = "npc_" + npcName.Replace(" ", "_").ToLower();
            generatedNpc.displayName = npcName;
            generatedNpc.personality = "Generated NPC";
            fullScene.npcs.Add(generatedNpc);

            SceneManager.Instance.RegisterPendingScene(locationName, fullScene);

            // Background
            StartCoroutine(ImageGenerator.Instance.GenerateSceneImage("Background of " + locationName, tex =>
            {
                fullScene.background = tex;
                pending.backgroundReady = true;
                if (fullPair != null) AnnouncementsManager.Instance.MarkAsLoaded(fullPair.bgUI);
                CheckSceneReady(locationName, pending, fullPair);
            }));

            // NPC
            StartCoroutine(ImageGenerator.Instance.GenerateNpcImage(
                $"Character sprite of {npcName}. Semi-realistic anime style, visual novel NPC. Transparent background PNG. NPC description: {reply.npc.description ?? ""}.",
                sprite =>
                {
                    generatedNpc.portrait = sprite;
                    pending.npcReady = true;
                    if (fullPair != null) AnnouncementsManager.Instance.MarkAsLoaded(fullPair.npcUI);
                    CheckSceneReady(locationName, pending, fullPair);
                }));
        }



        // ------------------------------------------------------------
        // HELPERS
        // ------------------------------------------------------------
        private void CheckSceneReady(string locationName, PendingScene pending, AnnouncementPair pair)
        {
            if (pending.IsReady())
            {
                Debug.Log($"[LLMParser] Scene fully ready: {locationName}");
                TryUnlockExit(pending.newScene, locationName, pending.originScene);

                if (pair != null)
                {
                    StartCoroutine(AnnouncementsManager.Instance.RemovePairAfterDelay(pair, 2.5f));
                }

                pendingScenes.Remove(locationName);
            }
        }

        private string SanitizeJsonString(string text)
        {
            if (string.IsNullOrEmpty(text)) return null;

            // Remove markdown fences (```json ... ```)
            text = text.Replace("```json", "")
                       .Replace("```", "")
                       .Trim();

            // Extract only the first valid JSON object
            int first = text.IndexOf('{');
            int last = text.LastIndexOf('}');
            if (first >= 0 && last > first)
                text = text.Substring(first, last - first + 1);

            return text.Trim();
        }

        private void TryUnlockExit(SceneData scene, string locationName, SceneData originScene)
        {
            if (originScene == null) originScene = SceneManager.Instance.GetCurrentScene();
            if (originScene == null) return;
            if (originScene.exits == null) originScene.exits = new List<string>();
            if (originScene.exits.Contains(locationName)) return;

            bool ready = scene.background != null && scene.npcs.Exists(n => n.portrait != null);
            if (ready)
            {
                originScene.exits.Add(locationName);
                SceneManager.Instance.RenderScene(true);
            }
        }
    }
}
